/**
 * \file
 */
#include "precompiled.hpp"
#include "kfs_fuse.hpp"
#include "kfs.hpp"
#include "kfs_cache.hpp"
#include "kfs_file_descriptor.hpp"
#include "kfs_impl.hpp"
#include "kfs_time.hpp"
#include "Exception.hpp"
#include <boost/bind.hpp>
#include <boost/program_options/variables_map.hpp>
#include <fuse.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>

using std::string;

// This static object is automatically zeroed out.
static struct fuse_operations myFuseOps;

static bool
kfsys_readdir_visitor(void* buf, fuse_fill_dir_t fuseVisitEntry,
    file_size_t nextEntryOffset,
    string const& entryName
);

static int kfsys_create(const char * const path_c, int flags, mode_t mode,
    FileDescriptor** fileDescriptor);


fuse_operations* kfsys_fuse_ops()
{
    myFuseOps.access = & kfsys_access;
    myFuseOps.create = & kfsys_create_regfile;
    myFuseOps.chmod = & kfsys_chmod;
    myFuseOps.chown = & kfsys_chown;
    myFuseOps.destroy = & kfsys_destroy;
    myFuseOps.getattr = & kfsys_getattr;
    myFuseOps.fgetattr = & kfsys_fgetattr;
    myFuseOps.init = & kfsys_init;
    myFuseOps.mkdir = & kfsys_create_dir;
    //myFuseOps.mknod = & kfsys_mknod;
    myFuseOps.open = & kfsys_open;
    myFuseOps.opendir = & kfsys_opendir;
    myFuseOps.read = & kfsys_read;
    myFuseOps.readdir = & kfsys_readdir;
    myFuseOps.release = & kfsys_release;
    myFuseOps.releasedir = & kfsys_releasedir;
    myFuseOps.rmdir = & kfsys_unlink;
    myFuseOps.truncate = &  kfsys_truncate;
    myFuseOps.ftruncate = & kfsys_ftruncate;
    myFuseOps.unlink = & kfsys_unlink;
    myFuseOps.utimens = & kfsys_utimens;
    myFuseOps.write = & kfsys_write;
    myFuseOps.flag_nullpath_ok = 1;           //< Use file handles (fi->fh)
    //myFuseOps. = & ;
    return &myFuseOps;
}


static inline
kfs* getInstance()
{
    // The context is only valid for the duration of a filesystem operation,
    // and thus must not be stored and used later.
    return reinterpret_cast<kfs*>(fuse_get_context()->private_data);
}


int kfsys_access(const char* path, int mode)
{
    kfs* const mykfs = getInstance();
    try
    {
        int flags = O_RDONLY;
        boost::scoped_ptr<FileDescriptor> file(mykfs->file_open(path, flags));
        Inode const& inode = file->inodeRef->elt->second->inode;
        /*
         * Either {R_OK, W_OK, X_OK} or {F_OK}.
         */
        if(mode == F_OK)
            return 0;
        // Assume there is only one user for now.
        if( (mode & R_OK) && !(inode.mode & S_IRUSR) )
            return -EACCES;
        if( (mode & W_OK) && !(inode.mode & S_IWUSR) )
            return -EACCES;
        if( (mode & X_OK) && !(inode.mode & S_IXUSR) )
            return -EACCES;
        return 0;
    }
    catch(Error& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -e.error();
    }
    catch(Exception& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -EPERM;
    }
}


int kfsys_chmod(const char *path, mode_t mode)
{
    kfs* const mykfs = getInstance();
    fuse_context const* ctx = fuse_get_context();
    try
    {
        int flags = O_RDONLY;
        boost::scoped_ptr<FileDescriptor> file(mykfs->file_open(path, flags));
        Inode& inode = file->inodeRef->elt->second->inode;
        /*
         * Is this user entitled to change this file?
         */
        if(ctx->uid != inode.userID && ctx->gid != inode.groupID &&
           ctx->uid != 0)
        {
            return -EACCES;
        }
        file->chmod(mode);
        return 0;
    }
    catch(Error& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -e.error();
    }
    catch(Exception& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -EPERM;
    }
}


int kfsys_chown(const char *path, uid_t uid, gid_t gid)
{
    kfs* const mykfs = getInstance();
    fuse_context const* ctx = fuse_get_context();
    try
    {
        int flags = O_RDONLY;
        boost::scoped_ptr<FileDescriptor> file(mykfs->file_open(path, flags));
        Inode& inode = file->inodeRef->elt->second->inode;
        /*
         * Is this user entitled to change this file?
         */
        if(ctx->uid != inode.userID && ctx->gid != inode.groupID &&
           ctx->uid != 0)
        {
            return -EACCES;
        }
        file->chown(uid, gid);
        return 0;
    }
    catch(Error& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -e.error();
    }
    catch(Exception& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -EPERM;
    }
}


int kfsys_create(const char * const path, int flags, mode_t mode,
    FileDescriptor** out_file)
{
    kfs* const mykfs = getInstance();
    try
    {
        fuse_context const* ctx = fuse_get_context();
        std::auto_ptr<FileDescriptor> file(
            mykfs->file_create(path, flags, mode, ctx->uid, ctx->gid
        ));
        if(out_file)
            *out_file = file.release();
        return 0;
    }
    catch(Error& e)
    {
        return -e.error();
    }
    catch(Exception& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -EPERM;
    }
}


int kfsys_create_dir(const char* const path_c, mode_t mode)
{
    /* Open the directory in read-write mode. This is necessary to initialize
     * its internal structure.
     */
    return kfsys_create(path_c, O_RDWR /*flags*/, mode|S_IFDIR, 0);
}


int kfsys_create_regfile(const char * const path_c, mode_t mode,
    fuse_file_info* fi
)
{
    FileDescriptor* file;
    int r = kfsys_create(path_c, fi->flags, mode, &file);
    // Open the new file.
    if(r == 0)
    {
        fi->fh = reinterpret_cast<uint64_t>(file);
    }
    else
    {
        fi->fh = 0;
        delete file;
    }
    return r;
}


void kfsys_destroy(void* user)
{
    kfs* instance = reinterpret_cast<kfs*>(user);
    delete instance;
}


void* kfsys_init(fuse_conn_info* conn)
{
    namespace po = boost::program_options;
    fuse_context const* const fc = fuse_get_context();
    po::variables_map const& options =
        *static_cast<po::variables_map*>(fc->private_data);

    // Is asynchronous read supported (read-write)
    conn->async_read = 0;

    try
    {
        string const& deviceName = options["device"].as<string>();
        kfs* fs = new kfs(deviceName);
        return fs;
    }
    catch(std::exception&e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        fuse_exit(fc->fuse);
        return 0;
    }
}


int kfsys_getattr(const char* path, struct stat* const mystat)
{
    kfs* const mykfs = getInstance();
    try
    {
        boost::scoped_ptr<FileDescriptor> file(
            mykfs->file_open(path, O_RDONLY));
        file->getattr(mystat);
        return 0;
    }
    catch(Error& e)
    {
        return -e.error();
    }
    catch(Exception& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -EPERM;
    }
}


int kfsys_fgetattr(const char* path, struct stat* const mystat,
    fuse_file_info* fi)
{
    if(fi->fh == 0)
    {
        return -EBADF; // Invalid file handle
    }

    kfs* const mykfs = getInstance();
    try
    {
        // Retrieve the inode.
        FileDescriptor* file = mykfs->impl->fd_deref(fi->fh);
        file->getattr(mystat);
        return 0;
    }
    catch(Error& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -e.error();
    }
    catch(Exception& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -EPERM;
    }
}


int kfsys_open(const char *path, struct fuse_file_info *fi)
{
    kfs* const mykfs = getInstance();
    try
    {
        std::auto_ptr<FileDescriptor> file(mykfs->file_open(path, fi->flags));
        Inode const& inode = file->inodeRef->elt->second->inode;
        if( S_ISDIR(inode.mode) )
        {
            return -EISDIR;
        }
        // Provide a file handle for later use.
        fi->fh = reinterpret_cast<uint64_t>(file.release());
        return 0;
    }
    catch(Error& e)
    {
        return -e.error();
    }
    catch(Exception& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -EPERM;
    }
}


int kfsys_opendir(const char *path, struct fuse_file_info *fi)
{
    /*
     * TODO: opendir should check if opendir is permitted for this directory.
     */
    kfs* const mykfs = getInstance();
    try
    {
        std::auto_ptr<FileDescriptor> file(mykfs->file_open(path, fi->flags));
        Inode const& inode = file->inodeRef->elt->second->inode;
        if( S_ISDIR(inode.mode) == 0 )
        {
            return -ENOTDIR;
        }
        // Provide a file handle for later use.
        fi->fh = reinterpret_cast<uint64_t>(file.release());
        return 0;
    }
    catch(Error& e)
    {
        return -e.error();
    }
    catch(Exception& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -EPERM;
    }
}


int kfsys_read(const char *path, char *destination, size_t maxNumBytes,
        off_t startPosition, struct fuse_file_info *fi)
{
    if(fi->fh == 0)
    {
        return -EBADF;
    }

    kfs* const mykfs = getInstance();
    try
    {
        // Retrieve the inode.
        FileDescriptor* file = mykfs->impl->fd_deref(fi->fh);

        size_t bytesRead = file->read(
            destination,
            maxNumBytes,
            startPosition
        );
        return bytesRead;
    }
    catch(Error& e)
    {
        return -e.error();
    }
    catch(Exception& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -EPERM;
    }
}


int kfsys_readdir(const char *path, void *buf, fuse_fill_dir_t fuseVisitEntry,
                  off_t offset, struct fuse_file_info *fi)
{
    if(fi->fh == 0)
    {
        return -EBADF;
    }

    kfs* const mykfs = getInstance();
    try
    {
        // Retrieve the inode.
        FileDescriptor* file = mykfs->impl->fd_deref(fi->fh);

        mykfs->impl->dir_visitEntries( file, offset,
            boost::bind( &kfsys_readdir_visitor, buf, fuseVisitEntry, _1, _2 )
        );
        return 0;
    }
    catch(Error& e)
    {
        return -e.error();
    }
    catch(Exception& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -EPERM;
    }
}


static bool
kfsys_readdir_visitor(
    void* buf,
    fuse_fill_dir_t fuseVisitEntry,
    file_size_t nextEntryOffset,
    string const& entryName
)
{
    /* Optionally, create a struct stat that describes the file as for getattr
     * (but FUSE only looks at st_ino and the file-type bits of st_mode).
     */
    struct stat* mystat = NULL;
    int rc = fuseVisitEntry(buf, entryName.c_str(), mystat, nextEntryOffset);
    // 0: success, 1: buffer is full or error occurred
    return rc == 0 ? true : false;
}


int kfsys_release(char const* path, fuse_file_info* fi)
{
    if(fi->fh == 0)
    {
        return -EBADF;
    }
    kfs* const mykfs = getInstance();
    mykfs->impl->fd_release(fi->fh);
    fi->fh = 0;
    return 0;
}


int kfsys_releasedir(char const* path, fuse_file_info* fi)
{
    if(fi->fh == 0)
    {
        return -EBADF;
    }
    kfs* const mykfs = getInstance();
    mykfs->impl->fd_release(fi->fh);
    fi->fh = 0;
    return 0;
}


int kfsys_truncate(const char* path, off_t newSize)
{
    kfs* const mykfs = getInstance();
    try
    {
        // Retrieve the inode.
        int flags = O_WRONLY;
        boost::scoped_ptr<FileDescriptor> file(mykfs->file_open(path, flags));
        Inode const& inode = file->inodeRef->elt->second->inode;
        if( S_ISDIR(inode.mode) )
            return -EISDIR;

        file->truncate(newSize);
        return 0;
    }
    catch(Error& e)
    {
        return -e.error();
    }
    catch(Exception& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -EPERM;
    }
}


int kfsys_ftruncate(const char* path, off_t newSize, fuse_file_info* fi)
{
    if(fi->fh == 0)
    {
        return -EBADF;
    }

    kfs* const mykfs = getInstance();
    try
    {
        // Retrieve the inode.
        FileDescriptor* file = mykfs->impl->fd_deref(fi->fh);
        Inode const& inode = file->inodeRef->elt->second->inode;
        if( S_ISDIR(inode.mode) )
            return -EISDIR;

        file->truncate(newSize);
        return 0;
    }
    catch(Error& e)
    {
        return -e.error();
    }
    catch(Exception& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -EPERM;
    }
}


int kfsys_unlink(char const* path)
{
    kfs* const mykfs = getInstance();
    try
    {
        mykfs->unlink(path);
        return 0;
    }
    catch(Error& e)
    {
        return -e.error();
    }
    catch(Exception& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -EPERM;
    }
}


int kfsys_utimens(const char * path, const struct timespec tv[2])
{
    kfs* const mykfs = getInstance();
    try
    {
        boost::scoped_ptr<FileDescriptor> file(
            mykfs->file_open(path, O_RDONLY));
        Inode& inode = file->inodeRef->elt->second->inode;

        inode.atime = KfsTime::toNanosec(tv[0]);
        inode.mtime = KfsTime::toNanosec(tv[1]);

        file->inodeRef->write();
        return 0;
    }
    catch(Error& e)
    {
        return -e.error();
    }
    catch(Exception& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -EPERM;
    }
}


int kfsys_write(const char* path, const char* source, size_t maxNumBytes,
    off_t startPosition, struct fuse_file_info* fi)
{
/*    if(fileHandle->flags & O_APPEND)
    {
        // TODO Does fuse take care of advancing fileStartPosition?
    }
*/
    if(fi->fh == 0)
    {
        return -EBADF;
    }
    kfs* const mykfs = getInstance();
    try
    {
        // Retrieve the inode.
        FileDescriptor* file = mykfs->impl->fd_deref(fi->fh);

        size_t numBytes = file->write(
            source,
            maxNumBytes,
            startPosition
        );
        return (numBytes == maxNumBytes) ? maxNumBytes : 0;
    }
    catch(Error& e)
    {
        return -e.error();
    }
    catch(Exception& e)
    {
        fprintf(stderr,"%s: %s\n", __FUNCTION__, e.what());
        return -EPERM;
    }
}
